/*
 * Sapphire backend
 *
 * Copyright (C) 2018 Alyssa Rosenzweig
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02111-1301  USA
 *
 */

#include <stdio.h>
#include <purple.h>

#include "core.h"
#include "websocket.h"

#define WS_PORT 7070
#define DEBUG_NET

/* This module is responsible for networking. WebSockets are provided via
 * libsoup. We use the libsoup API to create a server, as well as basic
 * connection management to abstract away networking from the libpurple-facing
 * code. */

/* Aside from our websocket responsibilities, it is necessary to serve some
 * static content, icons, etc. These callbacks follow */

GHashTable *username_to_icon;

/* Generic container for icons to paper over the difference between
 * PurpleStoredImage and PurpleBuddyIcon */

typedef struct {
	const gchar *extension;
	size_t size;
	gconstpointer data;
} SapphireIcon;

void
sapphire_add_buddy_icon(const gchar *name, PurpleBuddyIcon *icon)
{
	printf("Added up %s\n", name);

	SapphireIcon *sapphic = g_new0(SapphireIcon, 1);

	sapphic->extension = purple_buddy_icon_get_extension(icon);
	sapphic->data = purple_buddy_icon_get_data(icon, &sapphic->size);

	g_hash_table_insert(username_to_icon, g_strdup(name), sapphic);
}

void
sapphire_add_stored_image(const gchar *name, PurpleStoredImage *icon)
{
	printf("Added us %s\n", name);
	SapphireIcon *sapphic = g_new0(SapphireIcon, 1);

	sapphic->extension = purple_imgstore_get_extension(icon);
	sapphic->size = purple_imgstore_get_size(icon);
	sapphic->data = purple_imgstore_get_data(icon);

	g_hash_table_insert(username_to_icon, g_strdup(name), sapphic);
}

static void
soup_icon_callback(SoupServer *server,
                       SoupMessage *msg,
                       const char *path,
                       GHashTable *query,
                       SoupClientContext *client,
                       gpointer user_data)
{
	if (msg->method != SOUP_METHOD_GET) {
		soup_message_set_status (msg, SOUP_STATUS_NOT_IMPLEMENTED);
		return;
	}

	/* Extract the name from the query */

	if (!g_hash_table_contains(query, "name")) {
		printf("Nono\n");
		soup_message_set_status (msg, SOUP_STATUS_NOT_FOUND);
		return;
	}

	const gchar *name = g_hash_table_lookup(query, "name");
	printf("Name %s\n", name);

	/* Search for icon */
	SapphireIcon *icon = g_hash_table_lookup(username_to_icon, name);

	if (!icon) {
		printf("404\n");
		soup_message_set_status (msg, SOUP_STATUS_NOT_FOUND);
		return;
	}

	/* We found it, so return the icon appropriately */

	gchar *mimetype = g_strdup_printf("image/%s", icon->extension);

	if (!icon->data) {
		printf("Icon missing from purple?\n");
		soup_message_set_status (msg, SOUP_STATUS_NOT_FOUND);
		return;
	}

	soup_message_set_status(msg, SOUP_STATUS_OK);
	soup_message_set_response(msg, mimetype, SOUP_MEMORY_TEMPORARY, icon->data, icon->size);

	g_free(mimetype);
}

/* List of all authenticated connections. These connections will be broadcasted
 * to by broadcast_raw_packet. */

GList *authenticated_connections = NULL;

void
sapphire_send_raw_packet(Connection *conn, const char *packet)
{
	soup_websocket_connection_send_text(conn->connection, packet);

	/* Also, dump to stdout for debug */
#ifdef DEBUG_NET
	printf("> %s\n", packet);
#endif
}


/* Broadcast a packet to all currently connected clients. */
void
sapphire_broadcast_raw_packet(const char *packet)
{
	GList *l;

	/* Iterate the connection list and send message everywhere */

	for (l = authenticated_connections; l != NULL; l = l->next) {
		Connection *connection = (Connection *) l->data;
		sapphire_send_raw_packet(connection, packet);
	}
}

static void
soup_ws_data(SoupWebsocketConnection *self,
               gint                     type,
               GBytes                  *message,
               gpointer                 user_data)
{
	const gchar *frame = (const gchar *) g_bytes_get_data(message, NULL);
	printf("< %s\n", frame);

	/* The message should be interpreted as JSON, decode that here */

	JsonParser *parser = json_parser_new();

	if (!json_parser_load_from_data(parser, frame, -1, NULL)) {
		fprintf(stderr, "Error parsing response: %s\n", frame);
		return;
	}

	JsonNode *root = json_parser_get_root(parser);

	if (root == NULL) {
		printf("NULL root, ignoring\n");
		return;
	}

	struct Connection *conn = (struct Connection *) user_data;

	/* How to proceed depends if we're authenticated or not. If we are,
	 * this is a standard client message, ready to be parsed, relayed, and
	 * actuated. If we are not, this is an authentication message (by
	 * definition -- otherwise they get booted to penalize credential
	 * attacks */

	if (conn->is_authenticated) {
		JsonObject *obj = json_node_get_object(root);
		sapphire_process_message(conn, obj);
	} else {
		printf("Not Authed, authing\n");

		/* TODO: Check authentication */

		/* To authenticate, set the flag and add us to the list */
		conn->is_authenticated = TRUE;
		authenticated_connections = g_list_append(authenticated_connections, conn);

		/* Now that we're authenticated, it's necessary to bring the client up-to-date on what's popping */
		sapphire_send_world(conn);
	}
}

static void
soup_ws_error(SoupWebsocketConnection *self,
		GError *gerror,
               gpointer                 user_data)
{
	printf("WS Error\n");
}

static void
soup_ws_closed(SoupWebsocketConnection *self,
               gpointer                 user_data)
{
	/* Splice the socket out of the authenticated list, so we no longer
	 * attempt to broadcast to it */

	authenticated_connections = g_list_remove(authenticated_connections, user_data);
}

static void
soup_ws_callback(SoupServer *server,
			SoupWebsocketConnection *connection,
			const char *path,
			SoupClientContext *client,
			gpointer user_data)
{
	printf("Got websocket connection\n");

	/* Allocate a connection object for us and fill it in */
	Connection *conn = g_new0(Connection, 1);

	conn->is_authenticated = FALSE;

	/* Save the connection.
	 * IMPORTANT: Reference counting is necessary to keep the connection
	 * alive. No idea why this isn't documented anywhere, xxx
	 */

	conn->connection = g_object_ref(connection);

	/* Subscribe to the various signals */

	g_signal_connect(connection, "message", G_CALLBACK(soup_ws_data), conn);
	g_signal_connect(connection, "closed", G_CALLBACK(soup_ws_closed), conn);
	g_signal_connect(connection, "error", G_CALLBACK(soup_ws_error), conn);
}

void
sapphire_init_websocket(void)
{
	GError *error = NULL;

	SoupServer *soup = soup_server_new(NULL, NULL);

	/* Generic static callbacks */
	soup_server_add_handler(soup, "/icon/", soup_icon_callback, NULL, NULL);
	/* Initialize icon database */
	username_to_icon = g_hash_table_new(g_str_hash, g_str_equal);

	/* WebSocket entrypoint */
	char *protocols[] = { "binary", NULL };
	soup_server_add_websocket_handler(soup, "/ws", NULL, protocols, soup_ws_callback, NULL, NULL);

	if (!soup_server_listen_all(soup, WS_PORT, 0, &error)) {
		printf("Error listening in soup\n");
		printf("Msg: %s\n", error->message);
	}
}


